package evq.codec.core.managers.impl;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import evq.codec.core.entities.Dictionary;
import evq.codec.core.entities.Pgm;
import evq.codec.core.entities.PgmQuanta;
import evq.codec.core.entities.Quanta;
import evq.codec.core.entities.Quantum;
import evq.codec.core.entities.Vector;
import evq.codec.core.exceptions.InvalidFileFormatException;
import evq.codec.core.exceptions.SystemException;
import evq.codec.core.managers.FileManager;

public class DefaultFileManager implements FileManager {

	private static DefaultFileManager instance = new DefaultFileManager();
	
	private static final String CRLF = "\r\n";
	private static final String SPACE_STRING = " ";
	private static final String EMPTY_STRING = "";
	
	private static final String CODE_SEPARATOR = ": ";
	
	private static final String VQ_FILE_HEADER_LINE_1 = "================================================================================";
	private static final String VQ_FILE_HEADER_LINE_2 = "Educational vector quantization Codec 0.1";
	private static final String VQ_FILE_HEADER_LINE_3 = "================================================================================";
	
	private static final String VQ_DICTIONARY_FILE_HEADER_LINE_1 = "DI-SIZE=";
	private static final String VQ_DICTIONARY_FILE_HEADER_LINE_2 = "VQ-DIMENSION=";

	private static final String VQ_QUANTA_FILE_HEADER_LINE_1 = "TYPE=";
	
	private static final String VQ_PGM_QUANTA_FILE_HEADER_LINE_1 = "PGM-WIDTH=";
	private static final String VQ_PGM_QUANTA_FILE_HEADER_LINE_2 = "PGM-HEIGHT=";
	private static final String VQ_PGM_QUANTA_FILE_HEADER_LINE_3 = "PGM-GRAYSCALE=";
	
	private static final String VQ_QUANTA_FILE_TYPE_DEFAULT = "DEFAULT";
	private static final String VQ_QUANTA_FILE_TYPE_PGM = "PGM";
	
	private DefaultFileManager() {
		
	}
	
	public static DefaultFileManager getInstance() {
		return instance; 
	}
	
	public void createDictionaryFile(Dictionary dictionary, String filePath) {
		BufferedWriter writer = null;
		
		try {
			writer = new BufferedWriter(new FileWriter(filePath));
			
			writeVqCodecFileHeader(writer);
			 
			writer.write(VQ_DICTIONARY_FILE_HEADER_LINE_1 + dictionary.getSize() + CRLF);
			writer.write(VQ_DICTIONARY_FILE_HEADER_LINE_2 + dictionary.getQuantizatorDimension() + CRLF + CRLF);
			
			for (Integer code : dictionary.getCodes()) {
				writer.write(code + CODE_SEPARATOR);
				
				for (Double element : dictionary.getVector(code)) {
					writer.write(element + SPACE_STRING);
				}
				
				writer.write(CRLF);
			}
		} catch (IOException e) {
			throw new SystemException("Error trying to create the dictionary file: " + filePath, e);
		} finally {
			if (writer != null) {
				try {
					writer.flush();
					writer.close();
				} catch (IOException e) {
					throw new SystemException("Error trying to create the dictionary file: " + filePath, e);
				}
			}
		}
	}

	public Dictionary loadDictionaryFile(String filePath) {
		Dictionary dictionary = new Dictionary();

		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(filePath));

			readVqCodecFileHeader(reader);
			
			// Reading the dictionary size from file header
			String diSizeLine = reader.readLine();
			if (diSizeLine.indexOf(VQ_DICTIONARY_FILE_HEADER_LINE_1) != 0) {
				throw new InvalidFileFormatException("Invalid dictionary file format.");
			}
			
			int dictionarySize = Integer.parseInt(diSizeLine.substring(
					VQ_DICTIONARY_FILE_HEADER_LINE_1.length(), diSizeLine.length()));
			dictionary.setSize(dictionarySize);

			// Reading the quantizator dimension from file header
			String vqDimensionLine = reader.readLine();
			if (vqDimensionLine.indexOf(VQ_DICTIONARY_FILE_HEADER_LINE_2) != 0) {
				throw new InvalidFileFormatException("Invalid dictionary file format.");
			}
			
			int quantizatorDimension = Integer.parseInt(vqDimensionLine.substring(
					VQ_DICTIONARY_FILE_HEADER_LINE_2.length(), vqDimensionLine.length()));
			dictionary.setQuantizatorDimension(quantizatorDimension);
			
			// Reading the dictionary itself
			Scanner fileScanner = new Scanner(reader);
			fileScanner.nextLine();
			
			String codeVectorToken;
			int dictionaryEntries;
			
			for (dictionaryEntries = 0; (fileScanner.hasNextLine()); dictionaryEntries++) {
				codeVectorToken = fileScanner.nextLine();
				if (codeVectorToken.matches("[0-9]+:[0-9\\.E\\- ]*")) {
					int codeSeparatorIndex = codeVectorToken.indexOf(CODE_SEPARATOR);
					String codeString = codeVectorToken.substring(0,codeSeparatorIndex);

					int code = Integer.parseInt(codeString);
					
					String vectorToken = codeVectorToken.substring(codeSeparatorIndex + 2);
					Scanner vectorScanner = new Scanner(vectorToken);
					
					Vector vector = new Vector();
					String elementString;
					int vectorDimension;
					for (vectorDimension = 0; (vectorScanner.hasNext()); vectorDimension++) {
						elementString = vectorScanner.next();
						double element = Double.parseDouble(elementString);
						vector.addElement(element);
					}
					
					if (vectorDimension != quantizatorDimension) {
						throw new InvalidFileFormatException("Invalid dictionary file format.");
					}
					
					dictionary.addVector(code, vector);
				} else {
					throw new InvalidFileFormatException("Invalid dictionary file format.");
				}
			}
			
			if (dictionaryEntries != dictionarySize) {
				throw new InvalidFileFormatException("Invalid dictionary file format.");
			}
		} catch (NumberFormatException e) {
			throw new InvalidFileFormatException("Invalid dictionary file format.", e);
		} catch (IOException e) {
			throw new SystemException("Error trying to read the dictionary file: " + filePath, e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					throw new SystemException("Error trying to read the dictionary file: " + filePath, e);
				}
			}
		}
		
		return dictionary;
	}
	
	public void createPgmFile(Pgm pgm, String filePath) {
		BufferedWriter writer = null;
		
		try {
			writer = new BufferedWriter(new FileWriter(filePath));
						 
			writer.write(pgm.getHead());
			writer.write(CRLF);
			writer.write(SPACE_STRING + pgm.getWidth());
			writer.write(SPACE_STRING + pgm.getHeight());
			writer.write(CRLF);
			writer.write(SPACE_STRING + pgm.getGrayscale());
			writer.write(CRLF);
			
			for (List<Integer> line : pgm.getImage()) {
				for (Integer pixel : line) {
					writer.write(pixel + SPACE_STRING);
				}
				
				writer.write(CRLF);
			}
		} catch (IOException e) {
			throw new SystemException("Error trying to create the PGM file: " + filePath, e);
		} finally {
			if (writer != null) {
				try {
					writer.flush();
					writer.close();
				} catch (IOException e) {
					throw new SystemException("Error trying to create the PGM file: " + filePath, e);
				}
			}
		}
	}

	public Pgm loadPgmFile(String filePath) {
		Pgm pgm = new Pgm();
		
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(filePath));
			
			// Reading the head string
			String headToken = reader.readLine().trim();
			if (!headToken.matches("P2|P5")) {
				throw new InvalidFileFormatException("Invalid PGM file format.");
			}

			String head = headToken.substring(0, 2);
			pgm.setHead(head);
			
			// Reading the image width and height
			String widthHeightToken = reader.readLine().trim();
			if (!widthHeightToken.matches("[1-9][0-9]* [1-9][0-9]*")) {
				throw new InvalidFileFormatException("Invalid PGM file format.");
			}

			Scanner widthHeightScanner = new Scanner(widthHeightToken.trim());
			if (!widthHeightScanner.hasNext()) {
				throw new InvalidFileFormatException("Invalid PGM file format.");
			}
			
			int width = Integer.parseInt(widthHeightScanner.next());
			
			if (!widthHeightScanner.hasNext()) {
				throw new InvalidFileFormatException("Invalid PGM file format.");
			}
			
			int height = Integer.parseInt(widthHeightScanner.next());
			
			pgm.setWidth(width);
			pgm.setHeight(height);
			
			// Reading grayscale
			String grayScaleToken = reader.readLine().trim();
			if (!grayScaleToken.matches("[1-9][0-9]*")) {
				throw new InvalidFileFormatException("Invalid PGM file format.");
			}
			
			String grayScaleString = grayScaleToken;
			int grayScale = Integer.parseInt(grayScaleString);
			
			if (grayScale < 1 || grayScale > 65536) {
				throw new InvalidFileFormatException("Invalid PGM file format.");
			}
			
			pgm.setGrayScale(grayScale);
			
			//  Reading the image
			Scanner fileScanner = new Scanner(reader);
			
			List<List<Integer>> image = new ArrayList<List<Integer>>();
			while (fileScanner.hasNextLine()) {
				String lineString = fileScanner.nextLine();
				
				List<Integer> line = new ArrayList<Integer>();
				Scanner lineScanner = new Scanner(lineString);
				while (lineScanner.hasNext()) {
					int pixel = Integer.parseInt(lineScanner.next());
					line.add(pixel);
				}
				
				image.add(line);
			}
			
			pgm.setImage(image);
		} catch (NumberFormatException e) {
			throw new InvalidFileFormatException("Invalid PGM file format.", e);
		} catch (IOException e) {
			throw new SystemException("Error trying to read the PGM file: " + filePath, e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					throw new SystemException("Error trying to read the PGM file: " + filePath, e);
				}
			}
		}
		
		return pgm;
	}
	
	public void createQuantaFile(Quanta quanta, String filePath) {
		BufferedWriter writer = null;
		
		try {
			writer = new BufferedWriter(new FileWriter(filePath));
			
			writeVqCodecFileHeader(writer);
			
			if (quanta instanceof PgmQuanta) {
				PgmQuanta pgmQuanta = (PgmQuanta) quanta;
				
				writer.write(VQ_QUANTA_FILE_HEADER_LINE_1 + VQ_QUANTA_FILE_TYPE_PGM + CRLF);
				writer.write(VQ_PGM_QUANTA_FILE_HEADER_LINE_1 + pgmQuanta.getWidth() + CRLF);
				writer.write(VQ_PGM_QUANTA_FILE_HEADER_LINE_2 + pgmQuanta.getHeight() + CRLF);
				writer.write(VQ_PGM_QUANTA_FILE_HEADER_LINE_3 + pgmQuanta.getGrayScale() + CRLF);
				writer.write(CRLF);
			} else {
				writer.write(VQ_QUANTA_FILE_HEADER_LINE_1 + VQ_QUANTA_FILE_TYPE_DEFAULT + CRLF);
				writer.write(CRLF);
			}
			
			for (Quantum quantum : quanta) {
				writer.write(quantum.getValue() + SPACE_STRING);
			}
		} catch (IOException e) {
			throw new SystemException("Error trying to create the quanta file: " + filePath, e);
		} finally {
			if (writer != null) {
				try {
					writer.flush();
					writer.close();
				} catch (IOException e) {
					throw new SystemException("Error trying to create the quanta file: " + filePath, e);
				}
			}
		}
	}

	public Quanta loadQuantaFile(String filePath) {
		Quanta quanta = null;

		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(filePath));

			readVqCodecFileHeader(reader);
			
			String fileType = reader.readLine().trim();
						
			if (fileType.equals(VQ_QUANTA_FILE_HEADER_LINE_1 + VQ_QUANTA_FILE_TYPE_PGM)) {
				quanta = new PgmQuanta();
				
				// Reading the PGM width from file header
				String pgmWidthLine = reader.readLine().trim();
				if (pgmWidthLine.indexOf(VQ_PGM_QUANTA_FILE_HEADER_LINE_1) != 0) {
					throw new InvalidFileFormatException("Invalid quanta file format.");
				}
				
				int pgmWidth = Integer.parseInt(pgmWidthLine.substring(
						VQ_PGM_QUANTA_FILE_HEADER_LINE_1.length(), pgmWidthLine.length()));
				((PgmQuanta)quanta).setWidth(pgmWidth);
	
				// Reading the PGM height from file header
				String pgmHeightLine = reader.readLine().trim();
				if (pgmHeightLine.indexOf(VQ_PGM_QUANTA_FILE_HEADER_LINE_2) != 0) {
					throw new InvalidFileFormatException("Invalid quanta file format.");
				}
				
				int pgmHeight = Integer.parseInt(pgmHeightLine.substring(
						VQ_PGM_QUANTA_FILE_HEADER_LINE_2.length(), pgmHeightLine.length()));
				((PgmQuanta)quanta).setHeight(pgmHeight);
				
				// Reading the PGM gray scale from file header
				String pgmGrayScaleLine = reader.readLine().trim();
				if (pgmGrayScaleLine.indexOf(VQ_PGM_QUANTA_FILE_HEADER_LINE_3) != 0) {
					throw new InvalidFileFormatException("Invalid quanta file format.");
				}
				
				int pgmGrayScale = Integer.parseInt(pgmGrayScaleLine.substring(
						VQ_PGM_QUANTA_FILE_HEADER_LINE_3.length(), pgmGrayScaleLine.length()));
				((PgmQuanta)quanta).setGrayScale(pgmGrayScale);
			}
			
			reader.readLine();
			
			// Reading the quanta itself
			String quantaString = reader.readLine();
			Scanner fileScanner = new Scanner(quantaString);
			while (fileScanner.hasNext()) {
				int quantumInt = Integer.parseInt(fileScanner.next().trim());
				quanta.addQuantum(new Quantum(quantumInt));
			}
		} catch (NumberFormatException e) {
			throw new InvalidFileFormatException("Invalid quanta file format.", e);
		} catch (IOException e) {
			throw new SystemException("Error trying to read the quanta file: " + filePath, e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					throw new SystemException("Error trying to read the quanta file: " + filePath, e);
				}
			}
		}
		
		return quanta;
	}
	
	private void writeVqCodecFileHeader(BufferedWriter writer) throws IOException {
		writer.write(VQ_FILE_HEADER_LINE_1 + CRLF);
		writer.write(VQ_FILE_HEADER_LINE_2 + CRLF);
		writer.write(VQ_FILE_HEADER_LINE_3 + CRLF);
		writer.write(CRLF);
	}
	
	private void readVqCodecFileHeader(BufferedReader reader) throws IOException {
		if (!reader.readLine().equals(VQ_FILE_HEADER_LINE_1)) {
			throw new InvalidFileFormatException("Invalid file format.");
		}
		
		if (!reader.readLine().equals(VQ_FILE_HEADER_LINE_2)) {
			throw new InvalidFileFormatException("Invalid file format.");
		}
		
		if (!reader.readLine().equals(VQ_FILE_HEADER_LINE_3)) {
			throw new InvalidFileFormatException("Invalid file format.");
		}
		
		if (!reader.readLine().equals(EMPTY_STRING)) {
			throw new InvalidFileFormatException("Invalid file format.");
		}
	}
	
}
